/*
 * Copyright 2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package net.thick4.context;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.ServletContext;

import net.thick4.context.support.Wildcard;
import net.thick4.convention.Convention;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.ScannedGenericBeanDefinition;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 * コンテキストの再設定を実行するクラスです。
 * 再読み込み監視対象のSpring Bean定義XMLファイルを登録しておき、
 * リクエストがあるたびに再読み込みをおこないます。
 * 読み込み対象XMLファイル名のパターンをsetReloadableConfigLocation(String)メソッドで登録可能です。
 * なにも登録しない場合は、全てのXMLファイルが再読み込み対象になります。
 */
public class ReloadableContextReconfigurer implements Reloadable {
	
	/**
	 * Hotdeploy対象設定ファイルを指定するContextParameterのキー。 
	 */
	public static final String RELOADABLE_CONFIG_LOCATION_KEY = "reloadableConfigLocation";

	/**
	 * 再読み込み監視対象のSpring Bean定義XMLファイル名のデフォルト・パターンです。
	 */
	public static final String[] DEFAULT_RELODABLE_CONFIG_LOCATIONS = { "/WEB-INF/applicationContext-*.xml" };
	
	/**
	 * ログ出力用インスタンスです。
	 */
	private static Log log = LogFactory.getLog(ReloadableContextReconfigurer.class);
	
	/**
	 * ServletContext です。
	 */
	private ServletContext servletContext;

	/**
	 * 再読み込み監視対象の Spring Bean 定義XMLファイルの Map です。
	 */
	private Map monitoredXmls = new HashMap();
	
	/**
	 * 基準パッケージです。
	 */
	private String basePackage;
		
	/**
	 * 再読み込み監視対象のSpring Bean 定義 XMLファイル名のパターンです。
	 * デフォルト値は /WEB-INF/applicationContext-.*.xml です。
	 */
	private String[] reloadableConfigLocations = DEFAULT_RELODABLE_CONFIG_LOCATIONS;
	
	/**
	 * コンストラクタです。
	 * @param servletContext ServletContext
	 */
	public ReloadableContextReconfigurer(ServletContext servletContext) {
		this.servletContext = servletContext;
		this.basePackage = this.servletContext.getInitParameter(ReloadableXmlWebApplicationContext.BASE_PACKAGE_KEY);
		
		setRelodableConfigLocations(
			StringUtils.commaDelimitedListToStringArray(
				this.servletContext.getInitParameter(RELOADABLE_CONFIG_LOCATION_KEY)
			)
		);
		
		ReloadableUtil.add(this);
	}
	
	/**
	 * リクエストごとにコンテキストを再読み込みします。
	 */
	public void reload() {
		ReloadableXmlWebApplicationContext context = (ReloadableXmlWebApplicationContext) WebApplicationContextUtils.getWebApplicationContext(servletContext);

		// Spring 2.5.5からscan-componentで自動登録したコンポーネントは
		// 整合性チェックがかかってオーバーライドできないので、事前に破棄しておく。	
		DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
		Convention convention = (Convention) BeanFactoryUtils.beanOfType(beanFactory, Convention.class);
		
		String[] beanDefNames = beanFactory.getBeanDefinitionNames();
		for (int i=0; i<beanDefNames.length; ++i) {
			BeanDefinition beanDef = beanFactory.getBeanDefinition(beanDefNames[i]);
			if (beanDef instanceof ScannedGenericBeanDefinition
			 && convention.includeBasePackage(beanDef.getBeanClassName())) {
				beanFactory.removeBeanDefinition(beanDefNames[i]);
			}
		}
		
		String originalBasePackage = System.getProperty(ReloadableXmlWebApplicationContext.BASE_PACKAGE_KEY);
		try {
			if (basePackage != null) {
				System.setProperty(ReloadableXmlWebApplicationContext.BASE_PACKAGE_KEY, basePackage);
			}
			Iterator it = monitoredXmls.values().iterator();
			while (it.hasNext()) {
				ReloadableXML xml = (ReloadableXML) it.next();
				xml.reload();
			}
		} finally {
    		if (originalBasePackage != null) {
    			System.setProperty(ReloadableXmlWebApplicationContext.BASE_PACKAGE_KEY, originalBasePackage);
    		} else {
    			System.clearProperty(ReloadableXmlWebApplicationContext.BASE_PACKAGE_KEY);
    		}
		}
		
		context.initMessageSource();
	}
	
	/**
	 * 再読み込み監視対象のSpring Bean定義XMLファイルを登録します。
	 * 
	 * @param reader XML読み込み用のBeanDefinitionReader
	 * @param configLocation XMLファイルの場所を示す文字列
	 */
	public void registerXmlResources(BeanDefinitionReader reader, String[] configLocations) {
		if (configLocations != null) {
			for (int i = 0; i < configLocations.length; i++) {
				registerXmlResource(reader, configLocations[i]);
			}
		}
	}

	/**
	 * 再読み込み監視対象のSpring Bean定義XMLファイルを登録します。
	 * 
	 * @param reader XML読み込み用のBeanDefinitionReader
	 * @param configLocation XMLファイルの場所を示す文字列
	 */
	public void registerXmlResource(BeanDefinitionReader reader, String configLocation) {
		if (!monitoredXmls.containsKey(configLocation) && isRelodableConfigLocation(configLocation)) {
			log.info("Monitoring Spring bean definitions in '" + configLocation + "'.");
			monitoredXmls.put(configLocation, new ReloadableXML(reader, configLocation));
		}
	}
	
	/**
	 * 再読み込み対象のXMLファイル名のパターンを登録します。
	 * 
	 * @param location ファイル名パターン（正規表現）の配列
	 */
	public void setRelodableConfigLocations(String[] locations) {
		if (locations == null || locations.length == 0) {
			return;
		}

		Assert.noNullElements(locations, "Config locations must not be null");
		this.reloadableConfigLocations = new String[locations.length];
		for (int i = 0; i < locations.length; i++) {
			this.reloadableConfigLocations[i] = locations[i].trim();
		}
	}
	
	/**
	 * ファイル名が再読み込み対象のパターンと一致するかどうか判定します。
	 * 
	 * @param configLocation 検査対象ファイル名
	 * @return ファイル名が再読み込み対象のパターンのいずれかと一致したらtrueを返します
	 */
	protected boolean isRelodableConfigLocation(String configLocation) {
		if (reloadableConfigLocations == null) {
			return true;
		}
		
		for (int i=0; i<reloadableConfigLocations.length; ++i) {
			String pattern = reloadableConfigLocations[i];
			if (Wildcard.matches(pattern, configLocation)) {
				return true;
			}
		}
		return false;
	}
	
}